﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using Creventive.Wpf.ShellFactory.Logging;
using Creventive.Wpf.ShellFactory.Services;
using Creventive.Wpf.ShellFactory.Workspace;

namespace Creventive.Wpf.ShellFactory.Modules
{
    /// <summary>
    ///   An object containing information about a specific module. Common usage of this class is to add attached properties to it or start module-specific services.
    /// </summary>
    public class ApplicationModule : DependencyObject
    {
        #region Static Fields/Constants

        private static readonly Dictionary<ModuleInfo, ApplicationModule> ModuleCache = new Dictionary<ModuleInfo, ApplicationModule>();

        #endregion

        /// <summary>
        ///   Occurs when all modules have been loaded by the shell.
        /// </summary>
        public event EventHandler ShellLoaded;

        /// <summary>
        ///   Scans the given assembly for services and view models. If the module has an associated ApplicationModule class, this class is instantiated and returned.
        /// </summary>
        /// <param name = "moduleInfo">The module info.</param>
        /// <param name = "serviceContainer">The service container to add services to.</param>
        /// <returns>
        ///   A loaded ApplicationModule instance (if available).
        /// </returns>
        public static ApplicationModule Load(ModuleInfo moduleInfo, IServiceContainer serviceContainer = null)
        {
            if (moduleInfo == null)
                throw new ArgumentNullException("moduleInfo");

            ApplicationModule module;
            if (ModuleCache.TryGetValue(moduleInfo, out module))
                return module;

            var assemblyName = moduleInfo.AssemblyName;

            if (serviceContainer == null)
                serviceContainer = ServiceContainer.Default;

            var logger = Logger.Default;
            if (logger != null)
                logger.Info("Loading module " + assemblyName);

            // Attempt to retrieve the assembly instance
            Assembly assembly;
            try
            {
                assembly = Assembly.Load(assemblyName);
            }
            catch (Exception e)
            {
                if (logger != null)
                    logger.Error(e);
                throw new ModuleNotFoundException(assemblyName, e);
            }
            // Retrieve all assembly attributes
            Attribute[] attributes = assembly.GetCustomAttributes(false).Cast<Attribute>().ToArray();

            // Find and register all services
            int serviceCount = 0;
            ServiceAttribute[] unityAttributes = attributes.OfType<ServiceAttribute>().ToArray();
            foreach (var attribute in unityAttributes)
            {
                Type keyType = attribute.KeyType;
                Type instanceType = attribute.InstanceType;
                serviceContainer.RegisterServiceFactory(keyType, instanceType, attribute.Mode);
                serviceCount++;
            }
            if (logger != null)
            {
                if (serviceCount == 0)
                    logger.Info("No services found");
                else
                    logger.Info("Registered " + serviceCount + " services");
            }

            // Find and register all view model types
            int viewModelCount = 0;
            var viewModelAttributes = assembly.GetCustomAttributes(typeof(ApplicationViewModelAttribute), false).Cast<ApplicationViewModelAttribute>();
            foreach (var attribute in viewModelAttributes)
            {
                ApplicationViewModelFactory.RegisterModel(attribute.ViewModelType, attribute.ViewType);
                viewModelCount++;
            }
            if (logger != null)
            {
                if (viewModelCount == 0)
                    logger.Info("No view models found");
                else
                    logger.Info("Registered " + viewModelCount + " view models");
            }

            // Only try to load the module of dynamic modules.
            if (moduleInfo.ModuleType == ModuleType.Dynamic)
            {
                // Find the application module attribute, if any
                ApplicationModuleAttribute[] applicationModuleAttributes = attributes.OfType<ApplicationModuleAttribute>().ToArray();
                switch (applicationModuleAttributes.Length)
                {
                    case 0:
                        if (logger != null)
                            logger.Info("No module controller found");
                        // No application module attribute found
                        break;

                    case 1:
                    {
                        // Found the application module attribute, attempt to instantiate
                        Type moduleType = applicationModuleAttributes[0].ModuleType;
                        if (!typeof(ApplicationModule).IsAssignableFrom(moduleType))
                            throw new ModuleLoadException(assemblyName, "The application module attribute for the module " + assemblyName + " references the type " + moduleType.FullName + ", which does not derive from the " + typeof(ApplicationModule).FullName);

                        try
                        {
                            module = (ApplicationModule)Activator.CreateInstance(moduleType);
                            if (logger != null)
                                logger.Info("Loaded module controller");
                        }
                        catch (Exception e)
                        {
                            throw new ModuleLoadException(assemblyName, "Could not create an instance of the " + moduleType.FullName + " type as the module instance for the module " + assemblyName, e);
                        }
                        break;
                    }

                    default:
                        throw new ModuleLoadException(assemblyName, "The module " + assemblyName + " has multiple " + typeof(ApplicationModuleAttribute).FullName + " attributes");
                }
            }
            else
                module = moduleInfo.ApplicationModule;

            ModuleCache[moduleInfo] = module;
            if (logger != null)
                logger.Info("Loaded module " + assemblyName);

            return module;
        }

        internal void NotifyModulesLoaded()
        {
            this.OnShellLoaded();
        }

        /// <summary>
        ///   Called when all modules have been loaded by the shell.
        /// </summary>
        protected virtual void OnShellLoaded()
        {
            var handler = this.ShellLoaded;
            if (handler != null)
                handler.Invoke(this, EventArgs.Empty);
        }
    }
}